/**
 * - 作者: DoooReyn
 * - 日期: 2023.11.17
 * - 待办：
 *      - 优化 Root 属性
 *      - 考虑上架 Cocos Store
 */

import { assetManager, CCString, ImageAsset, Sprite, SpriteFrame, UITransform } from 'cc';
import { ccclass, ccmodifier, executeInEditMode, property, requireComponent } from '../../decorators';
import { LoggerComponent } from '../LoggerComponent';
import { RichTextParser } from './RichTextParser';
import { TextCommentStyle, TextStyle } from './RichTextStyle';

/**
 * - 作者: DoooReyn
 * - 日期: 2023.11.17
 * - 名称: HTMLRichText
 * - 描述: HTML 超级富文本组件
 * @description 标签说明
 * - richtext 根节点，负责富文本的整体配置，支持以下几个属性
 *      - align:  行内文本垂直方向对齐方式，可选值：top, center, bottom
 *      - background: 背景颜色
 *      - maxWidth: 绘制区域的最大宽度
 *      - margin: 绘制区域的外边距，表现形式：左,上,右,下
 *      - space: 文本的间距，表现形式：字间距,行间距
 *      - rowHeight: 默认行高，会影响 newline 和 blankline
 *      - font: 默认字体，详见 https://developer.mozilla.org/en-US/docs/Web/CSS/font
 *      - fillColor: 默认填充颜色
 *      - strokeColor: 默认描边颜色
 *      - lineWeight: 默认线条宽度
 * - text 文本节点，负责文本的样式配置，支持以下几个属性
 *      - value 文本内容
 *      - font: 字体，详见 https://developer.mozilla.org/en-US/docs/Web/CSS/font
 *      - color: 文本颜色
 *      - outline: 描边，表现形式：线条宽度,颜色
 *      - shadow: 阴影，表现形式：水平偏移,垂直偏移,模糊度,颜色
 *      - comment: 注释，表现形式：类型,颜色,宽度；目前支持以下几种注释样式：
 *          - none 无
 *          - underline 下划线
 *          - strikeout 删除线
 *          - waveline 波浪线
 *          - highlight 高亮
 *          - rect 矩形框
 *          - rroundect 圆角矩形框
 * - image 图片节点，负责图片的样式配置，支持以下几个属性
 *      - uuid 图片资源的 uuid
 *      - size 限制图片的尺寸，表现形式：宽,高
 * - newline 换行，负责换行
 * - blankline 空行，负责插入一行空白行
 * - outer 外部自定义节点，outer 不会绘制具体的内容，只会占用限定尺寸的空间，支持以下几个属性
 *      - size 限制该节点的尺寸，表现形式：宽,高
 *      - callback 回调函数名称，绘制该节点时会尝试调用该组件下指定的方法，可以借用这种方式灵活地插入或控制外部节点，常用于动态节点或交互节点，如：动画、按钮等
 * @example
 * ```xml
    <richtext align="center" background="#eedeb0" maxWidth="1200" margin="40,20,40,20" space="0,8" rowHeight="18" font="22px Andale Mono,STKaiti" lineWeight="2" fillColor="#383838" strokeColor="#361823">
        <text value="介绍" font="bold 28px STKaiti" color="#161823" />
        <newline/>
        <outer size="40,20" />
        <text value="HTMLRichText 是基于 Canvas 制作的一个富文本组件，采用 XML 格式定义富文本，其中：" />
        <text font="bold 22px Andale Mono" comment="highlight,#00ff003f" value=" richtext " />
        <text value="是根节点，它负责富文本的整体配置。" />
        <text value="子节点被根节点包裹，负责富文本的内容和样式呈现，它们分别是：" />
        <text font="bold 22px Andale Mono" comment="highlight,#ffff003f" value=" text " />
        <text value="文本节点，" />
        <text font="bold 22px Andale Mono" comment="highlight,#00ffff3f" value=" image " />
        <text value="图片节点，" />
        <text font="bold 22px Andale Mono" comment="highlight,#ff00003f" value=" outer " />
        <text value="外部自定义节点，" />
        <text font="bold 22px Andale Mono" comment="highlight,#8f00003f" value=" newline " />
        <text value="换行，以及 " />
        <text font="bold 22px Andale Mono" comment="highlight,#0f0f0f3f" value=" blank " />
        <text value=" 空行。" />
        <newline/>
        <outer size="40,20" />
        <text value="文本可以更换字体、字号、颜色，调整描边、阴影样式等；" />
        <text value="当然，也支持添加" />
        <text value="下划线、" comment="underline,#1685a9" />
        <text value="删除线、" comment="strikeout,#ff2d51" />
        <text value="波浪线" comment="waveline,#177cb0" />
        <text value="高亮、" comment="highlight,#faff723f" />
        <text value="矩形框、" comment="rect,#00bc12" />
        <text value="圆角矩形框" comment="roundrect,#e29c45,2" />
        <text value="等注释样式" />
        <text value="（开发者可以很容易地根据需要扩展注释样式）；" font="italic 18px STKaiti"/>
        <text value="图片可以限定尺寸；" />
        <text value="outer 可以作为占位节点，也可以用作自定义节点；" />
        <text value="此外，我们也支持行内不等高的节点，因此调整垂直方向的对齐方式" />
        <text value="（顶对齐、居中对齐、底对齐）" font="italic 18px STKaiti" />
        <text value="将会影响行内元素的表现形式；" />
        <text value="支持设置背景颜色，外边距，字间距和行间距。" />
        <newline/>
        <outer size="40,20" />
        <text value="接下来请看演示效果" />
        <text value="（本来就在演示了啊~~）" font="italic 18px STKaiti" />
        <text value="🙄🙄🙄。" />
        <blankline/>
        <text value="演示" font="bold 28px STKaiti" color="#161823" />
        <newline/>
        <outer size="40,20" />
        <text value="这是一段使用默认配置绘制的文字。" />
        <newline/>
        <outer size="40,20" />
        <text value="你可以根据需要将调整文本为" />
        <text value="华文黑体" font="22px 华文黑体" />
        <text value="，" />
        <text value="粗体" font="bold 22px STKaiti" />
        <text value="，" />
        <text value="28 号" font="28px Andale Mono,STKaiti" />
        <text value="，" />
        <text value="红色" color="red" />
        <text value="，" />
        <text value="描边" color="#ffb61e" outline="4,#222" />
        <text value="，" />
        <text value="阴影" color="#0aa344" shadow="0,0,4,#fff" />
        <text value="，" />
        <text value="下划线" comment="underline,green,2" />
        <text value="，" />
        <text value="删除线" comment="strikeout,red,2" />
        <text value="，" />
        <text value="波浪线" comment="waveline,blue,2" />
        <text value="，" />
        <text value="高亮" comment="highlight,#ff00003f" />
        <text value="，" />
        <text value="矩形框" comment="rect,#bddd22,2" />
        <text value="以及" />
        <text value="圆角矩形框" comment="roundrect,#2e4e7e,2" />
        <text value="。" />
        <text value="当然，在任意位置插入一张图片" />
        <outer size="10,20" />
        <image uuid="99ce2773-c288-4d67-9868-27bb8d5e9ea9" size="64,64" />
        <outer size="10,20" />
        <text value="也不是什么问题" />
        <text value="（HTMLRichText 使用 UUID 来拉取图片，所以它并不限制你使用何种方式加载图片，只要你确保图片在缓存中即可）。" font="italic 18px Andale Mono,STKaiti" />
        <text value="你还可以用 outer 插入一段自定义尺寸的空白区域" />
        <outer size="100,20" />
        <text value="，或者插入一个自定义节点：" />
        <outer size="48,56" callback="insertCustomNode" />
        <text value="。" />
        <text value="最牛的是，在没有外部自定义节点的情况下，该组件只会占用一个 DC，这对大量静态文本的渲染效率来说，无疑是大杀器！ " font="bold 24px Andale Mono,STKaiti" color="#ff7500" outline="3,#fff" />
        <newline/>
        <outer size="40,20" />
        <text value="HTMLRichText 目前发布了第一版，可能还有很多做的不好的地方，欢迎你提出宝贵的建议。(づ￣3￣)づ╭❤～😄" />
    </richtext>
 * ```
 */
@ccclass()
@ccmodifier('HTMLRichText')
@requireComponent(Sprite)
@executeInEditMode(true)
export class HTMLRichText extends LoggerComponent {
    /** 富文本解析器 */
    protected _parser: RichTextParser;

    /** 画布 */
    protected _canvas: HTMLCanvasElement;

    /** 画布渲染上下文 */
    protected _ctx: CanvasRenderingContext2D;

    /** 注释盒子 */
    protected _commentBox: CommentDivider;

    /** 自定义节点回调 */
    protected _customNodes: Function[];

    @property
    private _debugDraw: boolean = false;
    @property({ displayName: '开启调试绘制' })
    get debugDraw() {
        return this._debugDraw;
    }
    set debugDraw(v: boolean) {
        this._debugDraw = v;
        this.content = this._content;
    }

    @property({ type: Sprite, displayName: '富文本精灵' })
    get texture(): Sprite {
        return this.getComponent(Sprite)!;
    }

    @property
    private _content: string = '';
    @property({ type: CCString, displayName: '富文本内容（XML）', multiline: true })
    get content(): string {
        return this._content;
    }
    set content(v: string) {
        this._content = v;
        if (v.length === 0) {
            this.clear();
        } else {
            this._parser.parse(v);
            this.draw();
        }
    }

    protected onLoad() {
        this._customNodes = [];
        this._parser = new RichTextParser();
        this._canvas = document.createElement('canvas');
        this._commentBox = new CommentDivider(this);
    }

    protected start(): void {
        this.content = this._content;
    }

    clear() {
        this._ctx = this._ctx ?? this._canvas.getContext('2d');
        this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
        this.texture.spriteFrame = null;
    }

    draw() {
        if (this._parser.error) {
            this.clear();
            return;
        }

        /** 按行存储节点 */
        const rows: any[] = [];
        /** 按行存储行高 */
        const rowHeights = [];
        /** 提取解析参数 */
        const { children, style } = this._parser;
        /** 字间距、行间距 */
        const [spaceX, spaceY] = style.space;
        /** 外边距 */
        const [left, top, right, bottom] = style.margin;
        /** 当前行 */
        let rowIndex = 0;
        /** 当前行已累计的宽度 */
        let rowWidth = 0;
        /** 当前行已累计的高度 */
        let rowHeight = 0;

        /** 节点排列 */
        for (let i = 0; i < children.length; i++) {
            let child = children[i];
            if (child.type === 'newline') {
                /** 如果是换行，那么 rowIndex 需要加 1，并更新当前行高和行宽 */
                rowHeight = style.rowHeight;
                rowWidth = 0;
                ++rowIndex;
            } else {
                /** 如果是其他节点，则尝试将它放进当前行 */
                let [w, h] = child.size;
                if (rowWidth + w > style.maxWidth) {
                    /** 如果放不下，那么就新起一行，并更新当前行高和行宽 */
                    rowWidth = w + spaceX;
                    rowHeight = h;
                    ++rowIndex;
                } else {
                    /** 如果放得下，那么更新当前行高和行宽即可 */
                    rowWidth += w + spaceX;
                    rowHeight = Math.max(rowHeight, h);
                }
                /** 记录一下节点所属的行 */
                child.row = rowIndex;
            }
            /** 更新行高，取当前行最高的节点的高度 */
            rowHeights[rowIndex] = Math.max(style.rowHeight, rowHeight);
            /** 拿到当前行 */
            rows[rowIndex] = rows[rowIndex] ?? [];
            /** 把节点丢到当前行中 */
            rows[rowIndex].push(child);
        }

        /** 画布宽度=左外边距+右外边距+最大宽度 */
        const innerWidth = style.maxWidth;
        const width = left + right + innerWidth;

        const innerHeight = rowHeights.reduce((s, v) => s + v, 0) + (rowHeights.length - 1) * spaceY;
        /** 画布宽度=上外边距+下外边距+所有行的高度和行间距的总和 */
        const height = top + bottom + innerHeight;

        /** 设置画布尺寸 */
        this._canvas.width = width;
        this._canvas.height = height;

        /** 初始化渲染上下文 */
        this._ctx = this._canvas.getContext('2d')!;
        this._ctx.font = style.font;
        this._ctx.fillStyle = style.fillColor;
        this._ctx.strokeStyle = style.strokeColor;
        this._ctx.lineWidth = style.lineWeight;
        this._ctx.textBaseline = 'bottom';

        /** 清空画布 */
        this._ctx.clearRect(0, 0, width, height);

        /** 绘制背景 */
        this._ctx.save();
        this._ctx.fillStyle = style.background;
        this._ctx.fillRect(0, 0, width, height);
        this._ctx.restore();

        /** 平移画布 */
        this._ctx.translate(left, top);

        /** 清空注释分段 */
        this._commentBox.clear();

        /** 开始绘制 */
        for (let i = 0; i < rows.length; i++) {
            /** 当前行所有节点 */
            let childrenOfRow = rows[i];
            /** 当前节点的起始位置 X */
            let startXOfChild = 0;
            /** 当前节点的起始位置 Y */
            let startYOfChild = rowHeights.slice(0, i + 1).reduce((a, b) => a + b, 0) + i * spaceY;
            /** 当前行的高度 */
            let heightOfRow = rowHeights[i];
            /** 准备绘制节点 */
            for (let j = 0; j < childrenOfRow.length; j++) {
                /** 当前节点 */
                const child = childrenOfRow[j];
                /** 当前节点的宽、高 */
                const [cw, ch] = child.size;
                /** 垂直偏移量（用于对齐） */
                let offsetY = 0;
                if (style.align === 'top') {
                    offsetY = heightOfRow - ch;
                } else if (style.align === 'center') {
                    offsetY = (heightOfRow - ch) / 2;
                }
                /** 绘制节点 */
                switch (child.type) {
                    case 'text':
                        /** 绘制文本 */
                        this._ctx.save();
                        this._ctx.font = child.font;
                        let [sx, sy, sb] = child.shadow;
                        if (child.outline[0] > 0) {
                            this._ctx.lineWidth = child.outline[0];
                            this._ctx.strokeStyle = child.outline[1];
                            this._ctx.strokeText(child.text, startXOfChild, startYOfChild - offsetY);
                        }
                        if ([sx, sy, sb].every((v) => v === 0)) {
                            this._ctx.fillStyle = child.color;
                            this._ctx.fillText(child.text, startXOfChild, startYOfChild - offsetY);
                        } else {
                            this._ctx.fillStyle = child.color;
                            this._ctx.shadowOffsetX = child.shadow[0];
                            this._ctx.shadowOffsetY = child.shadow[1];
                            this._ctx.shadowBlur = child.shadow[2];
                            this._ctx.shadowColor = child.shadow[3];
                            this._ctx.fillText(child.text, startXOfChild, startYOfChild - offsetY);
                        }
                        this._ctx.restore();
                        /** 更新注释（目前只有文本才有注释功能） */
                        if (child.comment[0] !== 'none') {
                            /** 如果当前节点需要注释，那么就把它丢给注释分段工具处理 */
                            this._commentBox.enter(child, startXOfChild, startYOfChild, offsetY, heightOfRow);
                        } else {
                            /** 如果当前节点不需要注释，那么应该提交注释分段 */
                            this._commentBox.exit();
                        }
                        /** 别忘了更新下一个节点的起始位置 X */
                        startXOfChild += cw + spaceX;
                        break;
                    case 'image':
                        /** 绘制图片 */
                        const imageAsset = assetManager.assets.get(child.uuid);
                        if (imageAsset && imageAsset instanceof ImageAsset) {
                            /** 只有有效的图片才能被绘制 */
                            this._ctx.drawImage(
                                imageAsset.data as ImageBitmap,
                                startXOfChild,
                                startYOfChild - heightOfRow - offsetY,
                                cw,
                                ch
                            );
                            /** 别忘了更新下一个节点的起始位置 X */
                            startXOfChild += cw + spaceX;
                        }
                        break;
                    case 'outer':
                        /** 绘制外部节点 */
                        const callback = (this as any)[child.callback];
                        if (typeof callback === 'function') {
                            /** 拿到外部回调，尝试调用它（由于 Canvas 坐标系与 CC 坐标系不同，开发者需要自行转换） */
                            let x = startXOfChild,
                                y = startYOfChild,
                                h = heightOfRow,
                                size = child.size;
                            this._customNodes.push(() => callback.call(this, x, y, h, size));
                        }
                        /** 别忘了更新下一个节点的起始位置 X */
                        startXOfChild += cw + spaceX;
                        break;
                    case 'newline':
                    case 'blankline':
                        /** 换行和空行就不需要绘制了 */
                        break;
                }
            }

            /** 当前行结束时，要通知注释分段工具提交分段 */
            this._commentBox.exit();

            /** 绘制调试框 */
            if (this.debugDraw) {
                this._ctx.save();
                this._ctx.beginPath();
                this._ctx.strokeStyle = '#8888886f';
                this._ctx.lineWidth = 1;
                this._ctx.rect(0, startYOfChild - heightOfRow, style.maxWidth, heightOfRow);
                this._ctx.stroke();
                this._ctx.restore();
            }
        }

        if (this.debugDraw) {
            this._ctx.save();
            this._ctx.beginPath();
            this._ctx.strokeStyle = '#ef7a826f';
            this._ctx.lineWidth = 2;
            this._ctx.rect(0, 0, innerWidth, innerHeight);
            this._ctx.stroke();
            this._ctx.restore();
        }

        /** 将画布导出为 SpriteFrame 以展示富文本内容 */
        this.texture.spriteFrame = SpriteFrame.createWithImage(this._canvas);
        /** 我们的节点尺寸也要同步更新 */
        this.texture.node.getComponent(UITransform)?.setContentSize(width, height);
        /** 同步自定义节点（放在最后是因为节点尺寸是在最后更新的，如果在绘制时更新，最终的位置会出现严重的错误） */
        this._customNodes.forEach((v) => v.call(this));
        /** 最后别忘了清空它们 */
        this._customNodes.length = 0;
    }

    /**
     * 绘制注释样式（开发者可以在这里添加新的注释样式）
     * @param commentStyle 注释样式
     * @param commentColor 注释颜色
     * @param commentLine 注释线条宽度
     * @param startX 起始点 X
     * @param startY 起始点 Y
     * @param endX 结束点 X
     * @param offsetY 偏移量 Y
     * @param rowHeight 当前行的高度
     * @param cellHeight 节点的高度
     */
    drawComment(
        commentStyle: TextCommentStyle,
        commentColor: string,
        commentLine: number,
        startX: number,
        startY: number,
        endX: number,
        offsetY: number,
        rowHeight: number,
        cellHeight: number
    ) {
        this._ctx.save();
        const y = startY + commentLine - offsetY;
        switch (commentStyle) {
            case 'underline':
                /** 绘制下划线 */
                this._ctx.strokeStyle = commentColor;
                this._ctx.lineWidth = commentLine;
                this._ctx.beginPath();
                this._ctx.moveTo(startX, y);
                this._ctx.lineTo(endX, y);
                this._ctx.stroke();
                break;
            case 'strikeout':
                /** 绘制删除线 */
                this._ctx.strokeStyle = commentColor;
                this._ctx.lineWidth = commentLine;
                this._ctx.beginPath();
                this._ctx.moveTo(startX, y - cellHeight / 2);
                this._ctx.lineTo(endX, y - cellHeight / 2);
                this._ctx.stroke();
                break;
            case 'highlight':
                /** 绘制高亮 */
                this._ctx.fillStyle = commentColor;
                this._ctx.beginPath();
                this._ctx.fillRect(startX, y - cellHeight, endX - startX, cellHeight);
                break;
            case 'waveline':
                /** 绘制波浪线 */
                this._ctx.beginPath();
                this._ctx.lineWidth = commentLine;
                this._ctx.strokeStyle = commentColor;
                /** 一个波浪的宽度 */
                const piece = 14;
                /** 要画的波浪个数 */
                const segement = Math.round((endX - startX) / piece);
                this._ctx.moveTo(startX, y);
                for (let i = 0; i < segement; i++) {
                    startX += piece;
                    this._ctx.quadraticCurveTo(startX - piece / 2, y - commentLine * 2.5, startX, y);
                }
                this._ctx.stroke();
                break;
            case 'rect':
                /** 绘制矩形框 */
                this._ctx.beginPath();
                this._ctx.lineWidth = commentLine;
                this._ctx.strokeStyle = commentColor;
                this._ctx.strokeRect(startX, y - cellHeight, endX - startX, cellHeight);
                break;
            case 'roundrect':
                /** 绘制圆角矩形框 */
                this._ctx.beginPath();
                this._ctx.lineWidth = commentLine;
                this._ctx.strokeStyle = commentColor;
                const radii = Math.max(2, Math.min(10, Math.round(cellHeight * 0.15)));
                /** 测试发现编辑器模式下不支持圆角矩形，所以退回矩形代替 🙄🙄🙄 */
                const rect = this._ctx.roundRect ?? this._ctx.rect;
                rect.call(this._ctx, startX, y - cellHeight, endX - startX, cellHeight, radii);
                this._ctx.stroke();
                break;
        }
        this._ctx.restore();
    }
}

/**
 * 注释分段工具
 * @description 用于帮助注释分段
 */
class CommentDivider {
    /** 注释参数列表 */
    private _commentList: any[];

    /** @param _richText 富文本组件 */
    constructor(private _richText: HTMLRichText) {
        this._commentList = [];
    }

    /**
     * 添加一个注释节点到注释分段中
     * @param child 注释节点
     * @param startX 起始点 X
     * @param startY 起始点 Y
     * @param offsetY 偏移量 Y
     * @param rowHeight 当前行的高度
     */
    enter(
        child: TextStyle & { size: [number, number] },
        startX: number,
        startY: number,
        offsetY: number,
        rowHeight: number
    ) {
        // @ts-ignore
        if (!(this._commentList.length > 0 && this._commentList[0].comment.every((v, i) => v === child.comment[i]))) {
            /** 取第一个注释与当前注释节点的注释样式比较，如果不一样，则需要分段 */
            this.exit();
        }
        /** 往当前分段中加入注释参数 */
        this._commentList.push({ size: child.size, comment: child.comment, startX, startY, offsetY, rowHeight });
    }

    /** 提交分段并绘制 */
    exit() {
        if (this._commentList.length > 0) {
            /** 存在注释参数才需要绘制，注释样式以第一个为准（虽然都一样） */
            const { startX, startY, offsetY, rowHeight } = this._commentList[0];
            const [cs, cc, cl] = this._commentList[0].comment;
            const list = this._commentList.slice();
            const lastOne = list[list.length - 1];
            const [cw, ch] = lastOne.size;
            const endX = lastOne.startX + cw;
            this._richText.drawComment(cs, cc, cl, startX, startY, endX, offsetY, rowHeight, ch);
        }
        this.clear();
    }

    /** 清空分段 */
    clear() {
        this._commentList.length = 0;
    }
}
